管道是Shell中非常常用的东西,*nix的神奇,一大部分要归功于各式各样全能的小工具和管道。
简而言之,管道就是把若干个程序连接起来,一个管道符号的前一个程序的输出作为后一个程序的输入,比如,统计hello.c里面有多少行带有双斜杠可以这样写:
cat hello.c | grep '//' | wc -l |
这篇日志不是普及管道的,而是因为今天自己写Shell脚本时遇到了一个问题,记在这里作为提醒。
Shell中可以打开另外一个Shell,新打开的Shell就是Subshell,对于这个Subshell,打开它的Shell叫做Parent Shell。这两个Shell的最大区别在于环境变量,Parent Shell中所有被export过的环境变量会被Subshell继承,而Subshell对环境变量做的任何修改都不会影响Parent Shell中的环境变量。
在圆括号括起来的命令会在Subshell中执行:
export a=1; ( echo 'Sub 1: '$a; a=2; echo 'Sub 2: '$a; unset a; ); echo 'Parent: '$a |
会得到这样的结果
Sub 1: 1 Sub 2: 2 Parent: 1
今天,我写了一个脚本对一个ZOJ比赛的Runs页面进行不停地监视,一旦有新的状态产生,就用libnotify把它显示出来,这个脚本大致是这样的:
#!/bin/bash c=0 while true; do wget --load-cookies cookies.txt "http://xxxxxx" -qO a.html cat a.html | grep 'runId">[0-9]' -A 13 | sed 's/<[^>]*>//g;s/ //g' | uniq | while read i; do [ -n "$i" ] && i=`printf '%s' "$i" | tr -d '\r'` if [ "$i" = '--' ]; then c=0 elif [ -n "$i" ]; then (( c++ )) case "$c" in ('1') id=$i;; ('2') time="$i";; ('3') result="$i";; ('4') prob="$i";; ('5') lang="$i";; ('6') during="$i";; ('7') mem="$i";; ('8') user="$i"; if [ -z "${a[$id]}" ] && echo $result | grep -v 'ing' &>/dev/null; then a[$id]=1 notify-send "$prob - $user" "$result, ${during}ms ${mem}KB" fi ;; esac fi done sleep 10; done |
这个脚本运行起来会反复地显示获得的内容,也就是说if [ -z “${a[$id]}” ]检测被无视了。
为什么会这样子呢?经过调试,我发现bash遇到管道会创建一个Subshell,而zsh不会。
考虑一个简单的脚本,统计输入的行数:
c=0 while read; do (( c++ )); done echo 'Line count: '$c |
这是可以工作的,稍微修改一下:
c=0 cat | while read; do (( c++ )); done echo 'Line count: '$c |
在bash下就不能正常工作了,其中的c++会在Subshell中执行,导致最终结果是0,如果用zsh执行这段脚本仍然可以得到期待的结果。
那如何解决这个问题呢?把管道拆成两个重定向就可以啦 :-)
对于上面这段
cat | while read; do (( c++ )); done |
的写法,可以改成:
cat > tempfile while read; do (( c++ )); done < tempfile |
第一个脚本也可以类似地修改。
虽然直接使用zsh可以解决问题,但是由于zsh并不是每个地方都有的,而zsh也不是便于携带的,安装起来需要管理员权限往往自己没有,在这种情况下,对于类似这样的“有歧义”的写法,还是使用比较有“兼容性”的做法比较好。
当然,一味地追求“兼容性”也不是好事情。比如目前的Ubuntu/Debian发行版中,/bin/sh是链接到dash的。dash是一个极其轻量的Shell,一般zsh或者bash需要占用1到3MB的内存,而dash往往只需要几十KB的内存就可以工作了,没有自动补全,适合脚本使用。但是dash有严重问题,即便用它执行很简单的脚本。比如对于中文文件名,dash可能会在参数传递时出现编码问题,一个具体表现是使用for filename in *遍历时,把$filename当作参数传入cp这样的程序后,会说文件找不到 -.-b。
不追求“兼容性”,也不要迷信某一个程序会很“标准”,或者周围的人都在按照“标准”做事情。Ubuntu的Wiki上面说第一行是#!/bin/sh的脚本都应该兼容dash,这是个符合POSIX标准的Shell,但是实际情况绝对不是这样的。我使用Archlinux,把/bin/sh改成dash后,升级系统就发现系统无法启动了,折腾了好长时间才发现是这个原因导致的,Archlinux的kernel软件包的安装后执行脚本不兼容dash。
《三国演义》中第一句话就是“话说天下大势,分久必合,合久必分。”,期待一下zsh把Shell天下“合”的那一天
Bash 4 出来大有抢 zsh 用户之势,毕竟是默认 shell ,不过 Bash 4 能否有现在的 bash 这种优势还不一定,毕竟是很底层的东西,不能随便升级,Ubuntu 现在都还不肯把 Python 升级到 2.6 ,升级到不兼容性超级好的 Python 3 就更不用说了。
@pluskid: 经你这么一说我才发现有Bash 4,再一看,我现在用的已经是GNU bash, version 4.0.24(1)-release (i686-pc-linux-gnu)了。这样看来,对Bash的印象很不好 -.-|
@quark: 唔……没发现原来我的也已经升级到 Bash 了……
dash那个东西bug也不少吧。。。
还是最原始的那个posix shell最王道。。。
solaris一直是posix shell作为默认的。。。
Fantastic piece of writing, this is very similar to a site that I have. Please check it out sometime and feel free to leave me a comenet on it and tell me what you think. Im always looking for feedback.
@Laree Helt : 你也陪着他去小黑屋吧。
In the event the ditch cards are uncovered through the
option, a misdeal is known as and the seller will need to reshuffle and deal again.